<?php

/**
 * Converts strings to various formats.
 *
 * @author    Mon Zafra <monzee at gmail>
 * @copyright (c)2009 Mon Zafra
 * @category  Mz
 * @license   http://mz-project.googlecode.com/svn/trunk/LICENSE     MIT License
 * @version   SVN: $Id: Inflector.php 3 2009-02-18 18:55:27Z monzee $
 */
class Mz_Inflector
{
    const CHAR_DELIMITED = 1;
    const CAMEL_CASE = 2;
    const MIXED = 3;
    const INTERNAL_DELIMITER = '|';
    const AS_IS = 0;
    const TO_LOWER = 1;
    const TO_UPPER = 2;
    const UC_WORDS = 4;
    const UC_FIRST = 8;
    const LOWER_FIRST_WORD = 16;
    protected $_words;
    protected $_subject;
    protected $_splitMode;
    protected $_delimiters;

    public function __construct()
    {
        if (func_num_args() > 0) {
            $args = func_get_args();
            call_user_func_array(array($this, 'invoke'), $args);
        }
    }

    /**
     * For backwards compatibility.
     */
    public function invoke()
    {
        $args = func_get_args();
        return call_user_func_array(array($this, '__invoke'), $args);
    }

    /**
     * Magic invoke function for 5.3.
     *
     * @param mixed  $subject    String or array of words
     * @param int    $mode
     * @param string $delimiters
     * @return Mz_Inflector
     */
    public function __invoke($subject, $mode=self::CHAR_DELIMITED, $delimiters='_ -')
    {
        if (!is_array($subject)) {
            $this->setSubject($subject);
        } else {
            $this->setWords($subject);
        }
        $this->_splitMode = $mode;
        $this->_delimiters = $delimiters;
        return $this;
    }

    /**
     * Sets the subject string and clears the words array.
     *
     * @param string $subject
     * @return Mz_Inflector
     */
    public function setSubject($subject)
    {
        if ($subject !== $this->_subject) {
            $this->_subject = $subject;
            unset($this->_words);
        }
        return $this;
    }

    /**
     * Sets the words array.
     *
     * @param array $words
     * @return Mz_Inflector
     */
    public function setWords(array $words)
    {
        $this->_words = $words;
        $this->_subject = $this->join(' ');
        return $this;
    }

    /**
     * Splits the subject string into an array of words.
     *
     * @return array
     */
    public function getWords()
    {
        if (empty($this->_words)) {
            $delimiter = self::INTERNAL_DELIMITER;
            while (false !== strpos($this->_subject, $delimiter)) {
                $delimiter = '_' . $delimiter . '_';
            }
            $subject = $this->_subject;
            if ($this->_splitMode & self::CHAR_DELIMITED) {
                $subject = str_replace(str_split($this->_delimiters), $delimiter, $subject);
            }
            if ($this->_splitMode & self::CAMEL_CASE) {
                $subject = preg_replace('/(?<=[a-zA-Z])(?=[A-Z])/x', $delimiter, $subject);
            }
            $this->_words = explode($delimiter, $subject);
        }
        return $this->_words;
    }

    /**
     * Implodes words to string.
     *
     * @param string $glue      Word separator
     * @param int    $transform
     * @return string
     * @todo set subject and return $this instead so that it can be chained again
     */
    public function join($glue='', $transform=self::AS_IS)
    {
        $words = $this->getWords();
        if ($transform != self::AS_IS) {
            if ($transform & self::TO_LOWER) {
                $words = array_map('strtolower', $words);
            }
            if ($transform & self::TO_UPPER) {
                $words = array_map('strtoupper', $words);
            }
            if ($transform & self::UC_WORDS) {
                $words = array_map('ucwords', $words);
            }
            if ($transform & self::UC_FIRST) {
                $words[0] = ucfirst($words[0]);
            }
            if ($transform & self::LOWER_FIRST_WORD) {
                $words[0] = strtolower($words[0]);
            }
        }
        return implode($glue, $words);
    }

    /**
     * Converts plural to singular.
     * 
     * @return string
     * @todo improve logic
     */
    public function singularize()
    {
        $string = $this->_subject;
        $last3 = strtolower(substr($string, -3));
        $last2 = strtolower(substr($string, -2));
        $last  = strtolower(substr($string, -1));
        if ($last3 == 'ses' ) {
            return substr($string, 0, -2);
        } else if ($last == 's' && $last2 != 'ss') {
            return substr($string, 0, -1);
        } else {
            return $string;
        }
    }

    /**
     * Convert singular to plural.
     * 
     * @return string
     * @todo improve logic
     */
    public function pluralize()
    {
        $string = rtrim($this->_subject);
        return $string . ( strtolower(substr($string, -1)) == 's' ? 'es' : 's' );
    }

    /**
     * Shortcuts.
     *
     * @param string $method
     * @param array  $args
     * @return string|void
     */
    public function __call($method, $args)
    {
        $method = strtolower($method);
        if ('to' != substr($method, 0, 2)) {
            throw new BadMethodCallException("No such method ($method)");
        }
        $method = substr($method, 2);
        switch ($method) {
            case 'underscore':
                array_unshift($args, '_');
                return call_user_func_array(array($this, 'join'), $args);
                break;
            case 'hyphenated':
            case 'dashed':
                array_unshift($args, '-');
                return call_user_func_array(array($this, 'join'), $args);
                break;
            case 'normal':
            case 'phrase':
                array_unshift($args, ' ');
                return call_user_func_array(array($this, 'join'), $args);
                break;
            case 'camel':
            case 'camelcase':
                return $this->join('', self::TO_LOWER|self::UC_WORDS|self::LOWER_FIRST_WORD);
                break;
            case 'pascal':
            case 'pascalcase':
                return $this->join('', self::TO_LOWER|self::UC_WORDS);
                break;
            case 'headline':
            case 'title':
                return $this->join(' ', self::TO_LOWER|self::UC_WORDS);
                break;
            default:
                throw new BadMethodCallException("Invalid conversion method ($method)");
                break;
        }
    }

    /**
     * Returns the subject string.
     *
     * @return string
     */
    public function __toString()
    {
        return $this->_subject;
    }

}
